/*
This file is licensed to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.xmlunit.matchers;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.isEmptyString;
import static org.junit.Assert.assertThat;
import static org.xmlunit.TestResources.TEST_RESOURCE_DIR;
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo;
import static org.xmlunit.matchers.CompareMatcher.isSimilarTo;
import org.xmlunit.XMLUnitException;
import org.xmlunit.builder.Input;
import org.xmlunit.builder.Input.Builder;
import org.xmlunit.diff.Comparison;
import org.xmlunit.diff.Comparison.Detail;
import org.xmlunit.diff.ComparisonFormatter;
import org.xmlunit.diff.ComparisonListener;
import org.xmlunit.diff.ComparisonResult;
import org.xmlunit.diff.ComparisonType;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.DifferenceEvaluator;
import org.xmlunit.diff.ElementSelectors;
import org.xmlunit.util.Predicate;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.ComparisonFailure;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class CompareMatcherTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
/** set this to true for manual review of the Error Messages, how the really looks in your IDE. */
private final boolean letExceptionTestFail = false;
@Test
public void testIsIdenticalTo_withAssertionErrorForAttributes_throwsReadableMessage() {
// Expected Exception
expect(AssertionError.class);
expectMessage("Expected attribute value 'xy' but was 'xyz'");
expectMessage("at /Element[1]/@attr2");
expectMessage("attr2=\"xyz\"");
// run test:
assertThat("<Element attr2=\"xyz\" attr1=\"12\"/>", isIdenticalTo("<Element attr1=\"12\" attr2=\"xy\"/>"));
}
@Test
public void testIsIdenticalTo_withAssertionErrorForElementOrder_throwsReadableMessage() {
// Expected Exception
expect(AssertionError.class);
expectMessage("Expected child nodelist sequence '0' but was '1'");
expectMessage("comparing <b...> at /a[1]/b[1] to <b...> at /a[1]/b[1]");
// run test:
assertThat("<a><c/><b/></a>", isIdenticalTo("<a><b/><c/></a>")
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
}
@Test
public void testIsSimilarTo_withAssertionErrorForElementOrder_throwsReadableMessage() {
// run test:
assertThat("<a><c/><b/></a>", isSimilarTo("<a><b/><c/></a>")
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
}
@Test
public void testIsIdenticalTo_withAssertionErrorForWhitespaces_throwsReadableMessage() {
// Expected Exception
expect(AssertionError.class);
expectMessage("Expected child nodelist length '1' but was '3'");
expectMessage("<a>" + getLineSeparator() + " <b/>" + getLineSeparator() + "</a>");
expectMessage("<a><b/></a>");
// run test:
assertThat("<a>\n <b/>\n</a>", isIdenticalTo("<a><b/></a>"));
}
@Test
public void testIsIdenticalTo_withComparisonFailureForWhitespaces_throwsReadableMessage() {
// Expected Exception
expect(ComparisonFailure.class);
expectMessage("Expected child nodelist length '1' but was '3'");
expectMessage("expected:<<a>[<b/>]</a>> but was:<<a>[" + getLineSeparator() + " <b/>" + getLineSeparator() + "]</a>>");
// run test:
assertThat("<a>\n <b/>\n</a>", isIdenticalTo("<a><b/></a>").throwComparisonFailure());
}
@Test
public void testIsIdenticalTo_withIgnoreWhitespaces_shouldSucceed() {
// run test:
assertThat("<a>\n <b/>\n</a>", isIdenticalTo("<a><b/></a>").ignoreWhitespace());
}
@Test
public void testIsIdenticalTo_withIgnoreComments_shouldSucceed() {
// run test:
assertThat("<a><!-- test --></a>", isIdenticalTo("<a></a>").ignoreComments());
}
@Test
public void testIsIdenticalTo_withNormalizeWhitespace_shouldSucceed() {
// run test:
assertThat("<a>\n <b>\n Test\n Node\n </b>\n</a>", isIdenticalTo("<a><b>Test Node</b></a>")
.normalizeWhitespace());
}
@Test
public void testIsIdenticalTo_withNormalizeWhitespace_shouldFail() {
expect(AssertionError.class);
expectMessage("Expected text value 'TestNode' but was 'Test Node'");
// run test:
assertThat("<a>\n <b>\n Test\n Node\n </b>\n</a>", isIdenticalTo("<a><b>TestNode</b></a>")
.normalizeWhitespace());
}
@Test
public void testIsSimilarTo_withFileInput() {
expect(AssertionError.class);
expectMessage("In Source");
expectMessage("test2.xml");
// run test:
assertThat(new File(TEST_RESOURCE_DIR, "test1.xml"),
isSimilarTo(new File(TEST_RESOURCE_DIR, "test2.xml")));
}
@Test
public void testIsSimilarTo_withDifferenceEvaluator_shouldSucceed() {
// prepare testData
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
// run test
assertThat(test, isSimilarTo(control).withDifferenceEvaluator(new IgnoreAttributeDifferenceEvaluator("attr")));
}
@Test
public void testIsSimilarTo_withComparisonFormatter_shouldFailWithCustomMessage() {
// prepare testData
expect(AssertionError.class);
expectMessage("DESCRIPTION");
expectMessage("DETAIL-abc");
expectMessage("DETAIL-xyz");
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
// run test
assertThat(test, isSimilarTo(control).withComparisonFormatter(new DummyComparisonFormatter()));
}
@Test
public void testIsSimilarTo_withComparisonFormatterAndThrowComparisonFailure_shouldFailWithCustomMessage() {
// prepare testData
expect(ComparisonFailure.class);
expectMessage("DESCRIPTION");
expectMessage("DETAIL-[abc]");
expectMessage("DETAIL-[xyz]");
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
// run test
assertThat(test, isSimilarTo(control).withComparisonFormatter(new DummyComparisonFormatter()).throwComparisonFailure());
}
@Test
public void testIsSimilarTo_withComparisonListener_shouldCollectChanges() {
CounterComparisonListener comparisonListener = new CounterComparisonListener();
String controlXml = "<a><b>Test Value</b><c>ABC</c></a>";
String testXml = "<a><b><![CDATA[Test Value]]></b><c>XYZ</c></a>";
// run test:
try {
assertThat(testXml, isSimilarTo(controlXml).withComparisonListeners(comparisonListener));
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Expected text value 'ABC' but was 'XYZ'"));
}
// validate result
assertThat(comparisonListener.differents, is(1));
assertThat(comparisonListener.similars, is(1));
assertThat(comparisonListener.equals, is(Matchers.greaterThan(10)));
}
@Test
public void testIsSimilarTo_withDifferenceListener_shouldCollectChanges() {
CounterComparisonListener comparisonListener = new CounterComparisonListener();
String controlXml = "<a><b>Test Value</b><c>ABC</c></a>";
String testXml = "<a><b><![CDATA[Test Value]]></b><c>XYZ</c></a>";
// run test:
try {
assertThat(testXml, isSimilarTo(controlXml).withDifferenceListeners(comparisonListener));
Assert.fail("Should throw AssertionError");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Expected text value 'ABC' but was 'XYZ'"));
}
// validate result
assertThat(comparisonListener.differents, is(1));
assertThat(comparisonListener.similars, is(1));
assertThat(comparisonListener.equals, is(0));
}
@Test
public void testCompareMatcherWrapper_shouldWriteFailedTestInput() {
final String control = "<a><b attr=\"abc\"></b></a>";
final String test = "<a><b attr=\"xyz\"></b></a>";
// run test
final String fileName = "testCompareMatcherWrapper.xml";
try {
assertThat(test, TestCompareMatcherWrapper.isSimilarTo(control).withTestFileName(fileName));
Assert.fail("Should throw AssertionError");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Expected attribute value 'abc' but was 'xyz'"));
}
// validate that the written File contains the right data:
assertThat(new File(getTestResultFolder(), fileName), isSimilarTo(test));
}
@Test
public void testDiff_withAttributeDifferences() {
final String control = "<a><b attr1=\"abc\" attr2=\"def\"></b></a>";
final String test = "<a><b attr1=\"xyz\" attr2=\"def\"></b></a>";
try {
assertThat(test, isSimilarTo(control));
Assert.fail("Should throw AssertionError");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Expected attribute value 'abc' but was 'xyz'"));
}
assertThat(test, isSimilarTo(control)
.withAttributeFilter(new Predicate<Attr>() {
@Override
public boolean test(Attr a) {
return !"attr1".equals(a.getName());
}
}));
}
@Test
public void testDiff_withExtraNodes() {
String control = "<a><b></b><c/></a>";
String test = "<a><b></b><c/><d/></a>";
try {
assertThat(test, isSimilarTo(control));
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Expected child nodelist length '2' but was '3'"));
}
assertThat(test,
isSimilarTo(control)
.withNodeFilter(new Predicate<Node>() {
@Override
public boolean test(Node n) {
return !"d".equals(n.getNodeName());
}
}));
}
/**
* Really only tests there is no NPE.
* @see "https://github.com/xmlunit/xmlunit/issues/81"
*/
@Test(expected = AssertionError.class)
public void canBeCombinedWithFailingMatcher() {
assertThat("not empty", both(isEmptyString()).and(isIdenticalTo("")));
}
@Test
public void canBeCombinedWithPassingMatcher() {
assertThat("<a><c/><b/></a>", both(not(isEmptyString()))
.and(isSimilarTo("<a><b/><c/></a>")
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))));
}
@Test
public void usesDocumentBuilderFactory() throws Exception {
DocumentBuilderFactory dFac = Mockito.mock(DocumentBuilderFactory.class);
DocumentBuilder b = Mockito.mock(DocumentBuilder.class);
Mockito.when(dFac.newDocumentBuilder()).thenReturn(b);
Mockito.doThrow(new IOException())
.when(b).parse(Mockito.any(InputSource.class));
String control = "<a><b></b><c/></a>";
String test = "<a><b></b><c/><d/></a>";
try {
assertThat("<a><b></b><c/></a>",
not(isSimilarTo("<a><b></b><c/><d/></a>")
.withDocumentBuilderFactory(dFac)));
Assert.fail("Expected exception");
} catch (XMLUnitException ex) {
Mockito.verify(b).parse(Mockito.any(InputSource.class));
}
}
public void expect(Class<? extends Throwable> type) {
if (letExceptionTestFail) return;
thrown.expect(type);
}
public void expectMessage(String substring) {
if (letExceptionTestFail) return;
thrown.expectMessage(substring);
}
private String getLineSeparator() {
return System.getProperty("line.separator");
}
private final class DummyComparisonFormatter implements ComparisonFormatter {
@Override
public String getDetails(Detail details, ComparisonType type, boolean formatXml) {
return "DETAIL-" + details.getValue();
}
@Override
public String getDescription(Comparison difference) {
return "DESCRIPTION";
}
}
private final class IgnoreAttributeDifferenceEvaluator implements DifferenceEvaluator {
private final String attributeName;
public IgnoreAttributeDifferenceEvaluator(String attributeName) {
this.attributeName = attributeName;
}
@Override
public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
final Node controlNode = comparison.getControlDetails().getTarget();
if (controlNode instanceof Attr) {
Attr attr = (Attr) controlNode;
if (attr.getName().equals(attributeName)) {
return ComparisonResult.EQUAL;
}
}
return outcome;
}
}
private final class CounterComparisonListener implements ComparisonListener {
private int equals;
private int similars;
private int differents;
@Override
public void comparisonPerformed(Comparison comparison, ComparisonResult outcome) {
switch (outcome) {
case EQUAL:
equals++;
break;
case SIMILAR:
similars++;
break;
case DIFFERENT:
differents++;
break;
default:
break;
}
}
}
/**
* Example Wrapper for {@link CompareMatcher}.
* <p>
* This example will write the Test-Input into the Files System.<br>
* This could be useful for manual reviews or as template for a control-File.
*
*/
private static class TestCompareMatcherWrapper extends BaseMatcher<Object> {
private final CompareMatcher compareMatcher;
private String fileName;
protected TestCompareMatcherWrapper(CompareMatcher compareMatcher) {
this.compareMatcher = compareMatcher;
}
public TestCompareMatcherWrapper withTestFileName(String fileName) {
this.fileName = fileName;
return this;
}
public static TestCompareMatcherWrapper isSimilarTo(final Object control) {
return new TestCompareMatcherWrapper(CompareMatcher.isSimilarTo(control));
}
@Override
public boolean matches(Object testItem) {
if (fileName == null) {
return compareMatcher.matches(testItem);
}
// do something with your Test-Source
final Builder builder = Input.from(testItem);
// e.g.: write the testItem into the FilesSystem. So it can be used as template for a new control-File.
final File testFile = writeIntoTestResultFolder(builder.build());
return compareMatcher.matches(testFile);
}
private File writeIntoTestResultFolder(final Source source) throws TransformerFactoryConfigurationError {
FileOutputStream fop = null;
File file = new File(getTestResultFolder(), this.fileName);
try {
if (!file.exists()) {
file.createNewFile();
}
fop = new FileOutputStream(file);
marshal(source, fop);
fop.flush();
fop.close();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (fop != null) {
try {
fop.close();
} catch (IOException e) {
// ignore exception during close
}
}
}
return file;
}
private void marshal(final Source source, FileOutputStream fop)
throws TransformerFactoryConfigurationError, TransformerConfigurationException, TransformerException {
StreamResult r = new StreamResult(fop);
TransformerFactory fac = TransformerFactory.newInstance();
Transformer t = fac.newTransformer();
t.transform(source, r);
}
@Override
public void describeTo(Description description) {
compareMatcher.describeTo(description);
}
@Override
public void describeMismatch(Object item, Description description) {
compareMatcher.describeMismatch(item, description);
}
}
private static File getTestResultFolder() {
final File folder = new File(//
String.format("./target/testResults/%s", CompareMatcherTest.class.getSimpleName()));
if (!folder.exists()) {
folder.mkdirs();
}
return folder;
}
}